這裡是「Three.js學習日誌」的第19篇,這篇的內容是要來講解Raycasting的概念。這系列的文章假設讀者看得懂javascript,並且有Canvas 2D Context的相關知識。
今天我們來講講 ~ Three.js中用來偵測滑鼠懸停(Hover) / 點擊(Click) 的技巧: Raycasting(光線投射)
當我們在Google上面搜索Raycasting這個詞的時候,其實可以找到兩種方面的應用。
一種是關於3D圖像體積繪製的演算方法,另外一種則是遊戲中對於3D物件的命中檢測/點擊偵測,...等。
其實兩者背後的原理差不多,只是應用的面向不同而已。
| 3D圖像體積繪製演算 | 命中檢測/點擊偵測 | 
|---|---|
|  |  | 
而我們今天要講的內容跟後者比較有關。
如果要簡單解釋Raycasting這種技術在點擊偵測上的應用原理,我們可以想像當我們點擊螢幕的時候,螢幕會在滑鼠點擊的位置,沿著螢幕的法向量(也就是攝影機的朝向),去延伸出去一條無限長的射線。
而若這條射線與任何一個3D物體相交的話,該物體就會被判定為「被點擊」。

在Three.js中,如果想要做到mesh的懸停/點擊偵測,那就要使用Raycaster。
import {raycaster} from "https://cdn.skypack.dev/three";
const raycaster = new Raycaster();
// 從相機的朝向發出射線
// pointer是滑鼠映射再畫布上的位置,x和y的值都必須要用-1~+1之間的值來表示
raycaster.setFromCamera( pointer, camera );
// 把一整個scene的mesh通通檢測過一遍,看有沒有相交
const intersects = raycaster.intersectObjects( scene.children );
其實只要弄懂了
Raycasting的概念,那這段程式應該也不難理解才對。
除了物件點擊偵測之外,Three.js的Raycaster也可以用來偵測到底是點擊到物件的哪一個面,甚至是面上的哪一個座標位置。
接著我們會用一個簡單的範例來演練如何使用
Raycaster。
老樣子,初始化的過程就跳過了~
首先我們先在這個空白的Scene裡面,填入大量的Box Mesh,然後讓這些Box Mesh以隨機的方式分布。
這邊要特別注意
Material一定要By Loop去重新生成,不要讓所有Mesh共用用同一個材質實例。
...
const randomness = 10;//亂度係數
for (let i = 0; i < 300; i++) {
  const geo = new BoxGeometry(1, 1, 1, 10, 10, 10);
  const mat = new MeshStandardMaterial({
      color: new Color("rgba(255,0,0,0.1)"),
      transparent: true
  });
  const mesh = new Mesh(geo, mat);
  mesh.position.set(
    (Math.random() - 0.5) * 2 * randomness, 
    // 之所以要先減去0.5再乘以2,
    //是因為要讓方塊落在-randomness到+randomness的範圍
    (Math.random() - 0.5) * 2 * randomness,
    (Math.random() - 0.5) * 2 * randomness
  );
  scene.add(mesh);
}
接著我們需要像上一回一樣,透過addEventListener去抓取滑鼠座標,並把座標以Vector2的形式記錄下來。
const cursor = new Vector2();
// 這裡綁定的事件如果改成click就會變成點擊偵測了~
renderer.domElement.addEventListener("mousemove", (ev) => {
  const rect = renderer.domElement.getBoundingClientRect();
  // 透過把滑鼠的位置除以canvas寬度,來映射到垂直/水平 -1 ~ 1 的區間
  cursor.x = ((ev.clientX - rect.left) / rect.width - 0.5) * 2;
  cursor.y = -((ev.clientY - rect.top) / rect.height - 0.5) * 2;
  // 因為clientY和Three.js的坐標軸方向相反,所以記得要乘以-1
});
我們接著要對所有的方塊做滑鼠懸停的偵測,假如滑鼠移到某個方塊上,那我們就把那個方塊變成綠色的。
const rc = new Raycaster();
const loop = (time) => {
  rc.setFromCamera(cursor, camera);
  const intersects = rc.intersectObjects(scene.children);
  // 這邊我們必須要先刷新一次Scene裡面所有方塊的顏色,把它洗回去初始狀態
  scene.children.forEach((o) => {
    if (o instanceof Mesh) {
      o.material.color.set(0xff0000);
    }
  });
  // 接著再去把有跟射線相交的方塊洗成綠色
  intersects.forEach((o) => {
    o.object.material.color.set(0x00ff00);
  });
  controls.update();
  renderer.render(scene, camera);
  requestAnimationFrame((time) => {
    loop(time);
  });
};
loop();
搭拉~

codepen連結:點我
我們今天大致上介紹過了Raycasting還有Raycaster的用法~,而且今天也是Three.js與滑鼠互動操作章節的最後一篇了,希望大家可以繼續保持追蹤~